/*
-----------------------------------------------------------------------------
This source file is part of Labor3D
(Labor 3D Graphics Engine)

Copyright (c) 2009-2020 Rock Mao
* creator : Rock Mao
* created : 2009-6-27   20:09
* filename: LaborHardwareBuffer.h
-----------------------------------------------------------------------------
*/
#ifndef __LABORHARDWAREBUFFER_H__
#define __LABORHARDWAREBUFFER_H__
#include "../LaborCoreDef.h"
#include "LaborRenderSystemEnum.h"
NAMESPACE_LABOR3D_BEGIN

class LABORCORE_API CLHardwareBuffer
{
protected:
	size_t    mSizeInBytes;
	EHB_Usage mUsage;
	bool      mIsLocked;
	size_t    mLockStart;
	size_t    mLockSize;
	bool      mSystemMemory;
	bool      mUseShadowBuffer;
	CLHardwareBuffer* mpShadowBuffer;
	bool mShadowUpdated;
	bool mSuppressHardwareUpdate;

	/// Internal implementation of lock()
	virtual void* lockImpl(size_t offset, size_t length, EHB_LockOptions options) = 0;
	/// Internal implementation of unlock()
	virtual void unlockImpl(void) = 0;
public:
	CLHardwareBuffer(EHB_Usage usage, bool systemMemory, bool useShadowBuffer)
		: mUsage(usage), mIsLocked(false), mSystemMemory(systemMemory), 
		mUseShadowBuffer(useShadowBuffer), mpShadowBuffer(NULL), mShadowUpdated(false), 
		mSuppressHardwareUpdate(false) 
	{
		// If use shadow buffer, upgrade to WRITE_ONLY on hardware side
		if (useShadowBuffer && usage == HBU_DYNAMIC)
		{
			mUsage = HBU_DYNAMIC_WRITE_ONLY;
		}
		else if (useShadowBuffer && usage == HBU_STATIC)
		{
			mUsage = HBU_STATIC_WRITE_ONLY;
		}
	}
	virtual ~CLHardwareBuffer(){}

	/** Lock the buffer for (potentially) reading / writing.
	@param offset The byte offset from the start of the buffer to lock
	@param length The size of the area to lock, in bytes
	@param options Locking options
	@returns Pointer to the locked memory
	*/
	virtual void* lock(size_t offset, size_t length, EHB_LockOptions options)
	{
		assert(!isLocked() && "Cannot lock this buffer, it is already locked!");
		void* ret;
		if (mUseShadowBuffer)
		{
			if (options != eHBL_READ)
			{
				// we have to assume a read / write lock so we use the shadow buffer
				// and tag for sync on unlock()
				mShadowUpdated = true;
			}

			ret = mpShadowBuffer->lock(offset, length, options);
		}
		else
		{
			// Lock the real buffer if there is no shadow buffer 
			ret = lockImpl(offset, length, options);
			mIsLocked = true;
		}
		mLockStart = offset;
		mLockSize = length;
		return ret;
	}

	/** Lock the entire buffer for (potentially) reading / writing.
	@param options Locking options
	@returns Pointer to the locked memory
	*/
	void* lock(EHB_LockOptions options)
	{
		return this->lock(0, mSizeInBytes, options);
	}
	/** Releases the lock on this buffer. 
	@remarks 
	Locking and unlocking a buffer can, in some rare circumstances such as 
	switching video modes whilst the buffer is locked, corrupt the 
	contents of a buffer. This is pretty rare, but if it occurs, 
	this method will throw an exception, meaning you
	must re-upload the data.
	@par
	Note that using the 'read' and 'write' forms of updating the buffer does not
	suffer from this problem, so if you want to be 100% sure your
	data will not be lost, use the 'read' and 'write' forms instead.
	*/
	virtual void unlock(void)
	{
		assert(isLocked() && "Cannot unlock this buffer, it is not locked!");

		// If we used the shadow buffer this time...
		if (mUseShadowBuffer && mpShadowBuffer->isLocked())
		{
			mpShadowBuffer->unlock();
			// Potentially update the 'real' buffer from the shadow buffer
			_updateFromShadow();
		}
		else
		{
			// Otherwise, unlock the real one
			unlockImpl();
			mIsLocked = false;
		}

	}
	/** Reads data from the buffer and places it in the memory pointed to by pDest.
	@param offset The byte offset from the start of the buffer to read
	@param length The size of the area to read, in bytes
	@param pDest The area of memory in which to place the data, must be large enough to 
	accommodate the data!
	*/
	virtual void readData(size_t offset, size_t length, void* pDest) = 0;
	/** Writes data to the buffer from an area of system memory; note that you must
	ensure that your buffer is big enough.
	@param offset The byte offset from the start of the buffer to start writing
	@param length The size of the data to write to, in bytes
	@param pSource The source of the data to be written
	@param discardWholeBuffer If true, this allows the driver to discard the entire buffer when writing,
	such that DMA stalls can be avoided; use if you can.
	*/
	virtual void writeData(size_t offset, size_t length, const void* pSource,
		bool discardWholeBuffer = false) = 0;

	/** Copy data from another buffer into this one.
	@remarks
	Note that the source buffer must not be created with the
	usage HBU_WRITE_ONLY otherwise this will fail. 
	@param srcBuffer The buffer from which to read the copied data
	@param srcOffset Offset in the source buffer at which to start reading
	@param dstOffset Offset in the destination buffer to start writing
	@param length Length of the data to copy, in bytes.
	@param discardWholeBuffer If true, will discard the entire contents of this buffer before copying
	*/
	virtual void copyData(CLHardwareBuffer& srcBuffer, size_t srcOffset, 
		size_t dstOffset, size_t length, bool discardWholeBuffer = false)
	{
		const void *srcData = srcBuffer.lock(
			srcOffset, length, eHBL_READ);
		this->writeData(dstOffset, length, srcData, discardWholeBuffer);
		srcBuffer.unlock();
	}

	/// Updates the real buffer from the shadow buffer, if required
	virtual void _updateFromShadow(void)
	{
		if (mUseShadowBuffer && mShadowUpdated && !mSuppressHardwareUpdate)
		{
			// Do this manually to avoid locking problems
			const void *srcData = mpShadowBuffer->lockImpl(
				mLockStart, mLockSize, eHBL_READ);
			// Lock with discard if the whole buffer was locked, otherwise normal
			EHB_LockOptions lockOpt;
			if (mLockStart == 0 && mLockSize == mSizeInBytes)
				lockOpt = eHBL_WRITE_DISCARD;
			else
				lockOpt = eHBL_READ_WRITE;

			void *destData = this->lockImpl(
				mLockStart, mLockSize, lockOpt);
			// Copy shadow to real
			memcpy(destData, srcData, mLockSize);
			this->unlockImpl();
			mpShadowBuffer->unlockImpl();
			mShadowUpdated = false;
		}
	}

	/// Returns the size of this buffer in bytes
	size_t getSizeInBytes(void) const { return mSizeInBytes; }
	/// Returns the EHB_Usage flags with which this buffer was created
	EHB_Usage getUsage(void) const { return mUsage; }
	/// Returns whether this buffer is held in system memory
	bool isSystemMemory(void) const { return mSystemMemory; }
	/// Returns whether this buffer has a system memory shadow for quicker reading
	bool hasShadowBuffer(void) const { return mUseShadowBuffer; }
	/// Returns whether or not this buffer is currently locked.
	bool isLocked(void) const { 
		return mIsLocked || (mUseShadowBuffer && mpShadowBuffer->isLocked()); 
	}
	/// Pass true to suppress hardware upload of shadow buffer changes
	void suppressHardwareUpdate(bool suppress) {
		mSuppressHardwareUpdate = suppress;
		if (!suppress)
			_updateFromShadow();
	}

};
NAMESPACE_LABOR3D_END
#endif